package com.weibei.gateway.fiflt;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.weibei.common.constant.Constants;
import com.weibei.common.core.domain.Constant;
import com.weibei.common.core.domain.Result;
import com.weibei.common.core.domain.RetCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 网关鉴权
 * 注：
 * step1 校验ip白名单
 * step2 校验ip黑名单
 * step3 如果是调用验证码接口，校验是否ip发送验证码次数是否超限
 * step4 单个ip调用接口1分钟之内次数超限，自动加入黑名单（打印日志）
 * step5 如果是url白名单，则不校验token
 * step6 校验token
 */
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    /** ip白名单 **/
    @Value("${white.ip}")
    private String whiteIps;
    /** ip黑名单 **/
    @Value("${blackIp.allIp}")
    private String blackIps;
    /** 验证码每日限制次数(ip) **/
    @Value("${code.ipdaylimit}")
    private String codeIpDayLimit;
    /** 验证码每小时限制次数(ip) **/
    @Value("${code.iphourlimit}")
    private String codeIpHourlimit;
    /** 每分钟访问次数限制(ip) **/
    @Value("${request.minlimit.limit}")
    private String reqLimit;
    /** url白名单 **/
    @Value("${whiteUrl.allUrl}")
    private String whiteUrls;

    @Resource(name = "stringRedisTemplate")
    private ValueOperations<String, String> ops;
    @Resource(name = "stringRedisTemplate")
    private SetOperations<String, Object> setOps;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();
        String ip = exchange.getRequest().getHeaders().getFirst("X-Real-IP");
        log.info("url:{},ip：{}", url, ip);
        if (StringUtils.isEmpty(ip)){
            ip = "127.0.0.1";
        }

        //step1 校验是否ip白名单，如果是白名单，则不做其他校验
        Boolean isWhite = checkWhiteIp(ip);

        if (!isWhite) {

            //step2 校验ip黑名单
            Result result;
            result = checkBlackIp(ip);
            if (result.getErrno() != 200) {
                return setUnauthorizedResponse(exchange, result);
            }

            //step3 校验ip短信发送次数是否超限
            result = checkCodeLimitIp(url, ip);
            if (result.getErrno() != 200) {
                return setUnauthorizedResponse(exchange, result);
            }

//            //step4 校验不在此类型的白名单的ip，1分钟访问接口次数是否超限,如果超限，则自动加入系统黑名单
//            result = checkReqTimes(ip);
//            if (result.getErrno() != 200) {
//                return setUnauthorizedResponse(exchange, result);
//            }
        }

        //step5 验证是否为url白名单，如果是白名单，则不校验token，否则校验
        Boolean isUrlWhite = checkUrlWhite(url);
        if (isUrlWhite) {
            return chain.filter(exchange);
        }

        //step6 校验token
        String token = exchange.getRequest().getHeaders().getFirst(Constants.TOKEN);
        // token为空
        if (StringUtils.isBlank(token)) {
            return setUnauthorizedResponse(exchange,initReturnResult(RetCodeEnum.CODE_501,false,"token can't null or empty string"));
        }
        String userStr = ops.get(Constants.ACCESS_TOKEN + token);
        if (StringUtils.isBlank(userStr)) {
            return setUnauthorizedResponse(exchange,initReturnResult(RetCodeEnum.CODE_501,false,"token verify error"));
        }
        JSONObject jo = JSONObject.parseObject(userStr);
        String userId = jo.getString("userId");
        // 查询token信息
        if (StringUtils.isBlank(userId)) {
            return setUnauthorizedResponse(exchange, initReturnResult(RetCodeEnum.CODE_501,false,"token verify error"));
        }
        // 设置userId到request里，后续根据userId，获取用户信息
        String loginName = "";
        if (com.weibei.common.utils.StringUtils.isNotEmpty(jo.getString("loginName"))){
            loginName = jo.getString("loginName");
        }
        ServerHttpRequest mutableReq = exchange.getRequest().mutate().header(Constants.CURRENT_ID, userId)
                .header(Constants.CURRENT_USERNAME, loginName).build();
        ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
        return chain.filter(mutableExchange);
    }

    /**
     * 校验是否为ip白名单，如果是，则不做其他校验，直接返回
     * @param ip
     * @return
     */
    private Boolean checkWhiteIp(String ip) {
        String[] whiteArr = getArr(Constant.PARAM_IP_WHITE,whiteIps);
        Boolean isWhite = false;
        for (String whitip:whiteArr) {
            if (whitip.contains(ip)){
                isWhite = true;
                log.info("###ip:{}为白名单，不做任何校验，直接返回",ip);
                break;
            }
        }
        return isWhite;
    }

    /**
     * 验证是否为系统ip黑名单
     * 注：
     * 加入此黑名单的ip，不能访问系统任何接口
     * @param ip
     * @return
     */
    private Result checkBlackIp(String ip){
        //如果redis中未设置ip黑名单，则取配置中心的值，并将配置中心的配置存入redis
        String[] blackArr = getArr(Constant.PARAM_IP_BLACK,blackIps);
        //验证是否为ip黑名单
        for (String str:blackArr) {
            if(!StringUtils.isBlank(str)){
                str = str.replaceAll(" ", "");
                if (str.contains(ip)){
                    log.info("###ip:{}为黑名单，拒绝请求，直接返回",ip);
                    return initReturnResult(RetCodeEnum.CODE_406,false, "非法请求");
                }
            }
        }
        return initReturnResult(RetCodeEnum.CODE_200,true, null);
    }

    /**
     * 验证ip的短信验证码是否超限
     * @return
     */
    private Result checkCodeLimitIp(String url,String ip){
        if(!StringUtils.isBlank(ip)){
            if(url.equalsIgnoreCase(Constant.PARAM_SEND_CODE_URL)){
                if(!checkSendMsgHour(ip)){
                    return initReturnResult(RetCodeEnum.CODE_406,false, "验证码获取次数达上线，请一小时后再次尝试");
                }
                if(!checkSendMsgDay(ip)){
                    return initReturnResult(RetCodeEnum.CODE_406,false, "今日验证码获取次数达上线，24小时后再次尝试");
                }
            }
        }
        return initReturnResult(RetCodeEnum.CODE_200,true, null);
    }

    /**
     * 同一个ip在一个小时只能发送5次验证码
     * @param ip
     * @return
     */
    private boolean checkSendMsgHour(String ip){
        String remoteHourIpCnt = ops.get(Constants.REMOTE_HOUR_IP + ip);
        boolean result = true;
        if(StringUtils.isBlank(remoteHourIpCnt)){
            ops.set(Constants.REMOTE_HOUR_IP + ip, 1 + "", 60 * 60L, TimeUnit.SECONDS);
        }else{
            int remoteHourIpCntInt = Integer.parseInt(remoteHourIpCnt);
            //默认值，当redis未设置时，取默认值
            Integer limitH = Integer.valueOf(codeIpHourlimit);
            Object limitHObj = ops.get(Constants.REMOTE_HOUR_IP_LIMIT);
            if (!org.springframework.util.StringUtils.isEmpty(limitHObj)){
                limitH = Integer.valueOf(limitHObj.toString());
            }else {
                ops.set(Constants.REMOTE_HOUR_IP_LIMIT,limitH.toString());
            }
            if(remoteHourIpCntInt <= limitH){
                ops.set(Constants.REMOTE_HOUR_IP + ip, remoteHourIpCntInt + 1 + "", 60 * 60L, TimeUnit.SECONDS);
            }else{
                result = false;
                log.info("###ip:{},1小时内验证码次数超限，直接返回",ip);
            }
        }
        return result;
    }

    /**
     * 同一个ip一天只能发送10次验证的限制
     * @param ip
     * @return
     */
    private boolean checkSendMsgDay(String ip){
        String remoteDayIpCnt = ops.get(Constants.REMOTE_DAY_IP + ip);
        boolean result = true;

        if(StringUtils.isBlank(remoteDayIpCnt)){
            ops.set(Constants.REMOTE_DAY_IP + ip, 1 + "", 24 * 60 * 60L, TimeUnit.SECONDS);
        }else{
            int remoteDayIpCntInt = Integer.parseInt(remoteDayIpCnt);
            //默认值，当redis未设置时，取默认值
            Integer limitD = Integer.valueOf(codeIpDayLimit);
            Object limitDObj = ops.get(Constants.REMOTE_DAY_IP_LIMIT);
            if (!org.springframework.util.StringUtils.isEmpty(limitDObj)){
                limitD = Integer.valueOf(limitDObj.toString());
            }else {
                ops.set(Constants.REMOTE_DAY_IP_LIMIT,limitD.toString());
            }
            if(remoteDayIpCntInt <= limitD){
                ops.set(Constants.REMOTE_DAY_IP + ip, remoteDayIpCntInt + 1 + "",24 * 60 * 60L, TimeUnit.SECONDS);
            }else{
                result = false;
                log.info("###ip:{},1天内验证码次数超限，直接返回",ip);
            }
        }
        return result;
    }

    /**
     * 限制每分钟ip访问接口的次数
     * 注：
     * 1.如果是在此类型白名单，则访问接口不受限制
     * 2.如果不在，且超限，则自动加入此类型黑名单
     * @param ip
     * @return
     */
    private Result checkReqTimes(String ip){

        //默认值，当redis未设置时，取默认值
        Integer limit = Integer.valueOf(reqLimit);
        String limitStr = ops.get(Constant.PARAM_IP_REQ_LIMIT);
        if (!org.springframework.util.StringUtils.isEmpty(limitStr)){
            limit = Integer.valueOf(limitStr);
        }else {
            ops.set(Constant.PARAM_IP_REQ_LIMIT,limit.toString());
        }
        String ipReqTimesStr = ops.get(Constant.PARAM_IP_REQ_TIMES + ip);
        if(StringUtils.isBlank(ipReqTimesStr)){
            ops.set(Constant.PARAM_IP_REQ_TIMES + ip, 1 + "",60L, TimeUnit.SECONDS);
        }else{
            int ipReqTimes = Integer.parseInt(ipReqTimesStr);
            if(ipReqTimes <= limit){
                ops.set(Constant.PARAM_IP_REQ_TIMES + ip, ipReqTimes + 1 + "",60L, TimeUnit.SECONDS);
            }else{
                ops.set(Constant.PARAM_IP_REQ_TIMES + ip, ipReqTimes + 1 + "",60L, TimeUnit.SECONDS);
                //ip访问接口次数1分钟内超过限制，自动加入黑名单
                setOps.add(Constant.PARAM_IP_BLACK,ip);
                log.info("###{},1分钟内访问接口次数超过{}次,自动加入黑名单，直接返回",ip,limit);
                return initReturnResult(RetCodeEnum.CODE_406,false, "非法请求");
            }
        }
        return initReturnResult(RetCodeEnum.CODE_200,true, null);
    }

    /**
     * 校验是否是url白名单，如果是，则不作token验证，如果是，则需要token验证
     * @param url
     * @return
     */
    private Boolean checkUrlWhite(String url){
        String[] urlWhiteArr = getArr(Constant.PARAM_URL_WHITE,whiteUrls);
        // 跳过不需要验证的路径
        for (String str:urlWhiteArr) {
            if (str.contains(url)){
                return true;
            }
        }
        return false;
    }

    /**
     * 从redis中获取set数组，如果redis没有，则取配置中心，并保存到redis，最后转成数组返回
     * 注：仅适用于set集合
     * @param key
     * @return
     */
    private String[] getArr(String key, String configValue){
        String[] arr;
        //从redis中取出数据
        List<Object> redisList = new ArrayList<>();
        Cursor<Object> cursorip = setOps.scan(key, ScanOptions.NONE);
        while (cursorip.hasNext()){
            redisList.add(cursorip.next());
        }
        if (redisList.size() == 0){
            arr = configValue.split(Constant.PARAM_NOT_SEP);
            //redis未设置，则将配置文件中的配置保存到redis
            setOps.add(key,arr);
            return arr;
        }
        //list转数组
        List<String> list = (List<String>)(List)redisList;
        arr = new String[list.size()];
        list.toArray(arr);
        return arr;
    }

    /**
     * 构建返回
     * @param exchange
     * @param result
     * @return
     */
    private Mono<Void> setUnauthorizedResponse(ServerWebExchange exchange, Result result) {
        ServerHttpResponse originalResponse = exchange.getResponse();
        originalResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
        originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        byte[] response = null;
        try {
            response = JSON.toJSONString(result).getBytes(Constants.UTF8);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
        return originalResponse.writeWith(Flux.just(buffer));
    }

    /**
     * 构建Result对象
     * @param retCodeEnum
     * @param success
     * @param msg
     * @return
     */
    private Result initReturnResult(RetCodeEnum retCodeEnum, boolean success, String msg) {
        Result result = new Result();
        result.setErrno(retCodeEnum.getCode());
        if(org.springframework.util.StringUtils.isEmpty(msg)){
            result.setErrmsg(retCodeEnum.getDesc());
        }else{
            result.setErrmsg(msg);
        }
        result.setSuccess(success);
        return  result;
    }

    @Override
    public int getOrder() {
        return -200;
    }
}